/*
 * Copyright (c) 2013, Tomas Zima
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * 1) Redistributions of source code must retain the above copyright notice, this list
 *    of conditions and the following disclaimer.
 * 2) Redistributions in binary form must reproduce the above copyright notice, this list 
 *    of conditions and the following disclaimer in the documentation and/or other materials 
 *    provided with the distribution.
 * 3) Neither the name of the Tomas Zima nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific prior written
 *    permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package jgamio.framework.application;

import static org.lwjgl.opengl.GL11.*;

import java.util.ArrayList;
import java.util.List;

import jgamio.framework.application.KeyboardHandler.KeyEvent;
import jgamio.framework.application.exceptions.DisplayNotAvailable;
import jgamio.framework.application.exceptions.EnumDispatcherNotDefined;
import jgamio.framework.application.exceptions.InvalidConfigurationFile;

import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.PixelFormat;

/**
 * <p>
 * Implements basic application. Client can inherit this
 * class and override some methods to achieve different
 * functionality.
 * </p>
 * 
 * <p>
 * The recommended use is to inherit this class and override
 * constructor.
 * </p>
 * 
 * @author Tomas Zima
 */
public class BasicApplication
{
	/**
	 * For handling inputs from keyboard.
	 * 
	 * @see KeyboardHandler
	 */
	protected	KeyboardHandler	keyboardHandler					= new KeyboardHandler();
	
	/**
	 * Asserts enumerations to components (menu, game, ...).
	 */
	protected	EnumDispatcher	enumDispatcher					= new EnumDispatcher();
	
	/** Contains informations about component which is currently active. */
	protected	ComponentState	activeComponent					= ComponentState.MENU_ACTIVATED;
	
	/** Last count of FPS, which was measured. If none value was measured, it is -1. */
	private		int				lastFPSCount					= -1;
	
	/**
	 * <p>
	 * Last delta time, which was measured. If none value was measured, 
	 * it is -1.
	 * </p>
	 * 
	 * <p>
	 * Delta time is a time, which was needed for one iteration
	 * of the game loop. 
	 * </p>
	 */
	private		long			lastDeltaTime					= -1;
	
	/**
	 * How often should be updated count of FPS (in seconds).
	 * Count of FPS is always an average value during this period.
	 */
	protected	int				fpsUpdateTime					= 3;
	
	/**
	 * If (and how) show current value of FPS.
	 * 
	 * @see		#lastFPSCount
	 * @see		#fpsUpdateTime
	 * @see		ShowFPS
	 */
	private		ShowFPS					showFPS					= ShowFPS.WINDOW_TITLE;
	
	/** Width of the window (in pixels). */
	private		int						windowWidth;
	
	/** Height of the window (in pixels). */
	private		int						windowHeight;
	
	/** Title of the window. */
	private		String					windowTitle;
	
	/** Is full screen enabled? */
	private		boolean					windowFullScreen;
	
	/** Component, which represents menu. */
	protected	Component				componentMenu			= new Component(0);
	
	/** Component, which represents game. */
	protected	Component				componentGame			= new Component(1);
	
	/** Contains all components. */
	protected	ComponentsList			componentsList			= new ComponentsList();
	
	protected	KeyboardConfigLoader	keyboardConfigLoader	= new KeyboardConfigLoader(keyboardHandler, enumDispatcher, componentsList);

	/**
	 * <p>Constructor.</p>
	 * <p>Creates window, initializes OpenGL and enters game loop.</p>
	 * 
	 * @param windowWidth				Width of window (in pixels).
	 * @param windowHeight				Height of window (in pixels).
	 * @param windowTitle				Title of window.
	 * @param version					Version of OpenGL.
	 * @param subversion				Subversion of OpenGL.
	 * @throws DisplayNotAvailable 	Desired video mode is not available.
	 */
	public BasicApplication(int windowWidth, int windowHeight, String windowTitle, int version, int subversion) throws DisplayNotAvailable
	{
		this(windowWidth, windowHeight, windowTitle, version, subversion, true);
	}
	
	/**
	 * <p>
	 * Constructor.
	 * </p>
	 * 
	 * <p>
	 * Creates window, initializes OpenGL and enters game loop (if set so).
	 * This class is supposed to be called in overridden constructor, if you
	 * need to firstly call father's constructor via super(), then set something
	 * and finally enter game loop. If you use other constructor, game loop
	 * will be entered automatically and you loose up control, so you cannot
	 * set anything.
	 * </p>
	 * 
	 * @param windowWidth				Width of window (in pixels).
	 * @param windowHeight				Height of window (in pixels).
	 * @param windowTitle				Title of window.
	 * @param version					Version of OpenGL.
	 * @param subversion				Subversion of OpenGL.
	 * @param enterGameLoop				Enter game loop? True: yes, false: no.
	 * @throws DisplayNotAvailable	Desired video mode is not available. 
	 */
	public BasicApplication(int windowWidth,
							int windowHeight,
							String windowTitle,
							int version,
							int subversion,
							boolean enterGameLoop) throws DisplayNotAvailable
	{
		this(windowWidth, windowHeight, windowTitle, version, subversion, enterGameLoop, false);
	}
	
	/**
	 * <p>Constructor.</p>
	 * <p>Creates window, initializes OpenGL and enters game loop.</p>
	 * 
	 * @param windowWidth				Width of window (in pixels).
	 * @param windowHeight				Height of window (in pixels).
	 * @param windowTitle				Title of window.
	 * @throws DisplayNotAvailable	Desired videomode is not available. 
	 */
	public BasicApplication(int windowWidth, int windowHeight, String windowTitle) throws DisplayNotAvailable
	{
		this(windowWidth, windowHeight, windowTitle, 1, 0);
	}

	/**
	 * <p>
	 * Constructor.
	 * </p>
	 * 
	 * <p>
	 * Creates window, initializes OpenGL and enters game loop (if set so).
	 * This class is supposed to be called in overridden constructor, if you
	 * need to firstly call father's constructor via super(), then set something
	 * and finally enter game loop. If you use other constructor, game loop
	 * will be entered automatically and you loose up control, so you cannot
	 * set anything.
	 * </p>
	 * 
	 * <p>
	 * It also allows you to enter fullscreen.
	 * </p>
	 * 
	 * @param windowWidth		Width of window (in pixels).
	 * @param windowHeight		Height of window (in pixels).
	 * @param windowTitle		Title of window.
	 * @param version			Version of OpenGL.
	 * @param subversion		Subversion of OpenGL.
	 * @param enterGameLoop		Enter game loop? True: yes, false: no.
	 * @throws DisplayNotAvailable 
	 */	
	public BasicApplication(int windowWidth,
							int windowHeight,
							String windowTitle,
							int openglVersion,
							int openglSubversion,
							boolean enterGameLoop,
							boolean enterFullScreen) throws DisplayNotAvailable
	{
		this.createWindow(windowWidth, windowHeight, windowTitle, openglVersion, openglSubversion, enterFullScreen);
		
		this.windowWidth		= windowWidth;
		this.windowHeight		= windowHeight;
		this.windowTitle		= windowTitle;
		
		this.keyboardHandler.addListener(new DefaultListener());
		
		this.componentsList.add(componentGame);
		this.componentsList.add(componentMenu);
		
		if (enterGameLoop)
		{
			this.run();
		}
	}
	
	/**
	 * <p>
	 * Initializes OpenGL.
	 * </p>
	 * 
	 * <p>
	 * What it does:
	 * 	<ul>
	 * 		<li>sets visible part to the whole window (using glViewport)</li>
	 * 		<li>sets black background (using glClearColor)</li>
	 * 		<li>calls additionalOpenGLInit()</li>
	 * 	</ul>
	 * </p>
	 * 
	 * <p>
	 * It is recommended to override method additionalOpenGLInit(), as
	 * you probably would not need to change default behavior of this.
	 * </p>
	 * 
	 * @see #additionalOpenGLInit()
	 */
	protected final void initializeOpenGL()
	{
		glViewport(0, 0, Display.getDisplayMode().getWidth(), Display.getDisplayMode().getHeight());
		glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
		
		this.additionalOpenGLInit();
	}
	
	/**
	 * <p>
	 * This method is automatically called by initializeOpenGL().
	 * It sets coordinate system, so you can write coordinates in pixels
	 * (using glOrtho) by default.
	 * </p>
	 * 
	 * <p>
	 * It is supposed to be overridden by client to achieve some different
	 * OpenGL settings.
	 * </p>
	 */
	protected void additionalOpenGLInit()
	{
		glOrtho(0, Display.getDisplayMode().getWidth(), Display.getDisplayMode().getHeight(), 0, 1, -1);
	}
	
	/**
	 * <p>
	 * Implements game loop, which is running until the window is closed.
	 * </p>
	 * 
	 * <p>
	 * It is recommended to not override this method. However, if you will
	 * do that, you should do following things:
	 * 	<ul>
	 * 		<li>implement loop, something like: while(!Display.isCloseRequested())</li>
	 * 		<li>call method onRedraw()</li>
	 * 		<li>call method keyboardHandler.handleInputs()</li>
	 *  	<li>call Display.update()</li>
	 *  	<li>correctly measure delta time and determinate FPS</li>
	 *  	<li>call method onFPSUpdate when needed</li>
	 * 	</ul>
	 * </p>
	 * 
	 * <p>
	 * <b>Please remember, that this method is NOT supposed to be overridden.</b>
	 * You should rather use method onRedraw(), which is called in each
	 * iteration of the loop. This is probably enough for the most of cases.
	 * </p>
	 * 
	 * <p>
	 * However, if you really need to override this method for some reason,
	 * you can do that if all conditions described before are met. If you
	 * will miss some point, framework will loose up some of its functions.
	 * </p>
	 * 
	 * @see #onRedraw()
	 * @see #onFPSUpdate()
	 * @see #handleGameEvent(int)
	 * @see #handleMenuEvent(int)
	 * @see KeyboardHandler
	 */
	protected void run()
	{
		long	fpsPeriodStart			= System.currentTimeMillis();
		int		fpsPeriodRedrawCount	= 0;
		
		long	loopStartTime;
		
		// Until the window is closed
		while (!Display.isCloseRequested())
		{
			loopStartTime = System.nanoTime();
			
			// Redraw
			onRedraw();
			
			// Check keyboard inputs (look at 
			// KeyboardHandler for details).
			keyboardHandler.handleInputs();

			// Refresh window
			Display.update();
			
			// Determinate delta time
			this.lastDeltaTime	= System.nanoTime() - loopStartTime;
			
			// Increase count of redraws (for measuring of FPS)
			fpsPeriodRedrawCount++;
			
			/*
			 * Count of FPS will be updated according to value of fpsUpdateTime.
			 * This condition checks, if it is time for update.
			 * 
			 * fpsUpdateTime() is in seconds, so it have to be converted into
			 * milliseconds.
			 */
			if (System.currentTimeMillis() > fpsPeriodStart + 1000 * fpsUpdateTime)
			{
				// Determinate the average value of FPS
				this.lastFPSCount		= fpsPeriodRedrawCount / fpsUpdateTime;
								
				// Reinitialize variables
				fpsPeriodStart			= System.currentTimeMillis();
				fpsPeriodRedrawCount	= 0;
				
				// If we want to print value of FPS somewhere...
				onFPSUpdate();
			}
		}
	}
	
	/**
	 * This method will be called when new event for game
	 * was generated.
	 * 
	 * @param eventNumber	User defined event number.
	 */
	protected void handleGameEvent(KeyboardHandler.KeyEvent event)
	{
		//
	}

	/**
	 * This method will be called when new event for menu
	 * was generated.
	 * 
	 * @param eventNumber	User defined event number.
	 */
	protected void handleMenuEvent(KeyEvent event)
	{
		//
	}
	
	/**
	 * This method will be called when new event for any component
	 * is generated.
	 * 
	 * @param activeComponent2		Identifier of a component.
	 * @param event			User defined event.
	 */
	protected void handleKeyEvent(ComponentState activeComponent, KeyEvent event)
	{
		//
	}

	/**
	 * <p>
	 * This methods is called in each iteration of the
	 * game loop.
	 * </p>
	 * 
	 * <p>
	 * If you need to do something right in the game loop,
	 * overriding of this method should be the best way
	 * to do that.
	 * </p> 
	 */
	protected void onRedraw()
	{
		//
	}
	
	/**
	 * Sets if (and how) display the current value of FPS.
	 * 
	 * @see		ShowFPS
	 * @see		#onFPSUpdate()		
	 * @see		#showFPS
	 */
	protected final void setFPSDisplayer(ShowFPS showFPS)
	{
		this.showFPS = showFPS;
	}
	
	/**
	 * How often the value of FPS should be actualized?
	 * 
	 * @param updateTime	Length of period, for which
	 * 						will be measured average count of
	 * 						FPS (in seconds). 
	 */
	protected final void setFPSUpdateTime(int updateTime)
	{
		this.fpsUpdateTime = updateTime;
	}
	
	/**
	 * This method will be called each time the new value
	 * of FPS is measured.
	 */
	protected void onFPSUpdate()
	{
		switch (showFPS)
		{
			case NONE:
				break;
				
			case CONSOLE:
				System.out.println("FPS: " + lastFPSCount);
				break;
				
			case WINDOW_TITLE:
				Display.setTitle(windowTitle + " (FPS: " + lastFPSCount + ")");
				break;
				
			case CONSOLE_AND_TITLE:
				Display.setTitle(windowTitle + " (FPS: " + lastFPSCount + ")");
				System.out.println("FPS: " + lastFPSCount);
		}
	}
	
	/**
	 * Returns last measured value of FPS.
	 * 
	 * @return		Last measured value of FPS.
	 * @see			#lastFPSCount
	 */
	protected int getLastFPSCount()
	{
		return this.lastFPSCount;
	}
	
	/**
	 * Returns last measured value of delta time.
	 * 
	 * @return		Last measured value of delta time.
	 * @see			#lastDeltaTime
	 */
	protected long getLastDeltaTime()
	{
		return this.lastDeltaTime;
	}
	
	/**
	 * <p>
	 * Creates window and OpenGL context.
	 * </p>
	 * 
	 * <p>
	 * If there already exists some window, it will destroy it.
	 * This method might be used for changing of the resolution
	 * or size of the window during the runtime.
	 * </p>
	 * 
	 * @param windowWidth				Width of window (in pixels).
	 * @param windowHeight				Height of window (in pixels).
	 * @param windowTitle				Title of window.
	 * @param openglVersion				Version of OpenGL.
	 * @param openglSubversion			Subversion of OpenGL.
	 * @param enterFullScreen			Pass true, if you want to go fullscreen.
	 * @throws DisplayNotAvailable 	Desired video mode cannot be used.
	 */
	protected final void createWindow(
						int		windowWidth,
						int		windowHeight,
						String	windowTitle,
						int		openglVersion,
						int		openglSubversion,
						boolean	enterFullScreen) throws DisplayNotAvailable
	{
		// Destroy existing window
		if (Display.isCreated())
		{
			Display.destroy();
		}

		/*
		 * This construction could possibly end with error. 
		 */
		try
		{
			/*
			 * Enable fullscreen.
			 */
			if (enterFullScreen)
			{
				// Get list of all available display modes
				DisplayMode modes[] = Display.getAvailableDisplayModes();
				
				// Temporary variables
				int i = 0;
				boolean success = false;
				
				// Find first display mode with required width and height.
				for (; i < modes.length; i++)
				{
					// Rule #1: fullscreen available!
					if (!modes[i].isFullscreenCapable())
					{
						// If not available, skip this iteration.
						continue;
					}
					
					// Rule #2: required width and height and at least 16b depth
					if (modes[i].getWidth() == windowWidth && modes[i].getHeight() == windowHeight &&
						modes[i].getBitsPerPixel() >= 16)
					{
						success = true;
						break;
					}
				}

				// Print extremely sad message!
				if (!success)
				{
					throw new DisplayNotAvailable("Desired display mode is not available.");
				}
				// Set fullscreen videomode
				else
				{
					Display.setDisplayMode(modes[i]);
					Display.setFullscreen(true);
				}				
			}
			/*
			 * Create window (not at fullscreen).
			 */
			else
			{
				Display.setDisplayMode(new DisplayMode(windowWidth, windowHeight));
			}
			
			// Set new attributes
			this.windowWidth		= windowWidth;
			this.windowHeight		= windowHeight;
			this.windowTitle		= windowTitle;
			this.windowFullScreen	= enterFullScreen;
			
			// Set window title
			Display.setTitle(windowTitle);

			/*
			 * Set graphic context attributes
			 */
			// configures bit depth etc
			PixelFormat pixelFormat	= new PixelFormat(); 
			
			// configures OpenGL version
			ContextAttribs contextAtrributes = new ContextAttribs(openglVersion, openglSubversion);
			
			// creates window
			Display.create(pixelFormat, contextAtrributes);
			
			// initialize OpenGL (some basic settings)
			this.initializeOpenGL();
		}
		catch (LWJGLException errHandler)
		{
			throw new DisplayNotAvailable("Window cannot be created.");
		}
	}
	
	/**
	 * Returns width of current window (or resolution in the
	 * X axis in the case of fullscreen mode).
	 * 
	 * @return		Width of window (in pixels).
	 * @see			#createWindow(int, int, String, int, int, boolean)
	 * @see			#windowWidth
	 */
	protected final int getWindowWidth()
	{
		return this.windowWidth;
	}
	
	/**
	 * Returns height of current window (or resolution in the
	 * Y axis in the case of fullscreen mode)
	 * 
	 * @return		Height of window (in pixels).
	 * @see			#createWindow(int, int, String, int, int, boolean)
	 * @see			#windowHeight
	 */
	protected final int getWindowHeight()
	{
		return this.windowHeight;
	}
	
	/**
	 * Return true if window is in fullscreen mode. False in other
	 * cases.
	 * 
	 * @return		True / false : Is in fullscreen / Not in fullscreen
	 */
	protected final boolean isFullscreenEnabled()
	{
		return this.windowFullScreen;
	}
	
	/**
	 * Returns title of the window.
	 * 
	 * @return		Title of window.
	 */
	protected final String getWindowTitle()
	{
		return this.windowTitle;
	}
	
	/**
	 * Asserts enum to specified component.
	 */
	protected final void addEnumDispatcher(Component component, ClientEnumParent<?> enumInstance)
	{
		this.enumDispatcher.addEnumDispatcher(component, enumInstance);
	}
	
	/**
	 * Loads XML file with configuration of the keyboard. It only calls
	 * a method loadConfigFile() from object {@link #keyboardConfigLoader}.
	 * 
	 * @param	filename					Fully specified location of your file.

	 * @throws	InvalidConfigurationFile	Specified file is not valid configuration keyboard file. 
	 * @throws	EnumDispatcherNotDefined	Enumeration was not defined for some component.
	 * 
	 * @see		KeyboardConfigLoader
	 */
	protected final void loadKeyboardConfig(String filename) throws EnumDispatcherNotDefined, InvalidConfigurationFile
	{
		this.keyboardConfigLoader.loadConfigFile(filename);
	}
	
	/**
	 * Returns instance of an enum dispatcher.
	 * 
	 * @return	Instance of enum dispatcher.
	 */
	protected EnumDispatcher getEnumDispatcher()
	{
		return this.enumDispatcher;
	}
	
	/**
	 * Sets game as active component.
	 */
	protected void setGameActive()
	{
		this.activeComponent = ComponentState.GAME_ACTIVATED;
		this.keyboardHandler.setActiveComponent(componentGame);
	}
	
	/**
	 * Sets menu as active component.
	 */
	protected void setMenuActive()
	{
		this.activeComponent = ComponentState.MENU_ACTIVATED;
		this.keyboardHandler.setActiveComponent(componentMenu);
	}
	
	/**
	 * @return	Reference of an active component.
	 */
	protected Component getActiveComponent()
	{
		if (isGameActive())
		{
			return this.componentGame;
		}
		else
		if (isMenuActive())
		{
			return this.componentMenu;
		}
		else
		{
			return null;
		}
	}
	
	protected boolean isGameActive()
	{
		if (this.activeComponent == ComponentState.GAME_ACTIVATED)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	
	protected boolean isMenuActive()
	{
		if (this.activeComponent == ComponentState.MENU_ACTIVATED)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	
	/**
	 * Information about state of component.
	 * 
	 * @author Tomas Zima
	 */
	protected enum ComponentState
	{
		GAME_ACTIVATED,
		MENU_ACTIVATED
	}
	
	/**
	 * All possibilities of showing the current value
	 * of FPS.
	 * 
	 * @author	Tomas Zima
	 * @see		#lastFPSCount
	 * @see		#fpsUpdateTime
	 */
	protected enum ShowFPS
	{
		/**
		 * Ignore. Do not print current count of FPS.
		 */
		NONE,
		
		/**
		 * Print current count of FPS into the console
		 * (standard output).
		 */
		CONSOLE,
		
		/**
		 * Add current count of FPS into window's
		 * title.
		 */
		WINDOW_TITLE,
		
		/**
		 * Print current count of FPS into the console
		 * (standard output) and also add it into
		 * the title of window.
		 * 
		 * @see		ShowFPS.CONSOLE
		 * @see		ShowFPS.WINDOW_TITLE
		 */
		CONSOLE_AND_TITLE
	}
	
	/**
	 * <p>
	 * Default listener, which will listen for generated keyboard events.
	 * </p>
	 * 
	 * <p>
	 * It will call methods for handling inputs in components game
	 * and menu.
	 * </p>
	 * 
	 * @author Tomas Zima
	 */
	protected class DefaultListener implements KeyboardHandler.Listener
	{
		@Override
		public void onKeyEvent(KeyEvent event)
		{
			if (isGameActive())
			{
				BasicApplication.this.handleGameEvent(event);
			}
			else
			if (isMenuActive())
			{
				BasicApplication.this.handleMenuEvent(event);
			}
			else
			{
				BasicApplication.this.handleKeyEvent(activeComponent, event);
			}
		}
	}
}
